﻿using Microsoft.WindowsAPICodePack.ShellExtensions.Resources;
using System;
using System.IO;
using System.Runtime.InteropServices;
using System.Runtime.InteropServices.ComTypes;

namespace Microsoft.WindowsAPICodePack.ShellExtensions
{
    /// <summary>Options for commiting (flushing) an IStream storage stream</summary>
    [Flags]
    internal enum StorageStreamCommitOptions
    {
        /// <summary>Uses default options</summary>
        None = 0,

        /// <summary>Overwrite option</summary>
        Overwrite = 1,

        /// <summary>Only if current</summary>
        OnlyIfCurrent = 2,

        /// <summary>Commits to disk cache dangerously</summary>
        DangerouslyCommitMerelyToDiskCache = 4,

        /// <summary>Consolidate</summary>
        Consolidate = 8
    }

    /// <summary>A wrapper for the native IStream object.</summary>
    public class StorageStream : Stream, IDisposable
    {
        private readonly bool _isReadOnly = false;
        private IStream _stream;

        internal StorageStream(IStream stream, bool readOnly)
        {
            if (stream == null)
            {
                throw new ArgumentNullException("stream");
            }
            _isReadOnly = readOnly;
            _stream = stream;
        }

        /// <summary>Gets whether the stream can be read from.</summary>
        public override bool CanRead => _stream != null;

        /// <summary>Gets whether seeking is supported by the stream.</summary>
        public override bool CanSeek => _stream != null;

        /// <summary>Gets whether the stream can be written to. Always false.</summary>
        public override bool CanWrite => _stream != null && !_isReadOnly;

        /// <summary>Gets the length of the IStream</summary>
        public override long Length
        {
            get
            {
                ThrowIfDisposed();
                const int STATFLAG_NONAME = 1;
                _stream.Stat(out var stats, STATFLAG_NONAME);
                return stats.cbSize;
            }
        }

        /// <summary>Gets or sets the current position within the underlying IStream.</summary>
        public override long Position
        {
            get
            {
                ThrowIfDisposed();
                return Seek(0, SeekOrigin.Current);
            }
            set
            {
                ThrowIfDisposed();
                Seek(value, SeekOrigin.Begin);
            }
        }

        /// <summary>Commits data to be written to the stream if it is being cached.</summary>
        public override void Flush() => _stream.Commit((int)StorageStreamCommitOptions.None);

        /// <summary>Reads a buffer worth of bytes from the stream.</summary>
        /// <param name="buffer">Buffer to fill</param>
        /// <param name="offset">Offset to start filling in the buffer</param>
        /// <param name="count">Number of bytes to read from the stream</param>
        /// <returns></returns>
        public override int Read(byte[] buffer, int offset, int count)
        {
            ThrowIfDisposed();

            if (buffer == null) { throw new ArgumentNullException("buffer"); }
            if (offset < 0) { throw new ArgumentOutOfRangeException("offset", LocalizedMessages.StorageStreamOffsetLessThanZero); }
            if (count < 0) { throw new ArgumentOutOfRangeException("count", LocalizedMessages.StorageStreamCountLessThanZero); }
            if (offset + count > buffer.Length) { throw new ArgumentException(LocalizedMessages.StorageStreamBufferOverflow, "count"); }

            var bytesRead = 0;
            if (count > 0)
            {
                var ptr = Marshal.AllocCoTaskMem(sizeof(ulong));
                try
                {
                    if (offset == 0)
                    {
                        _stream.Read(buffer, count, ptr);
                        bytesRead = (int)Marshal.ReadInt64(ptr);
                    }
                    else
                    {
                        var tempBuffer = new byte[count];
                        _stream.Read(tempBuffer, count, ptr);

                        bytesRead = (int)Marshal.ReadInt64(ptr);
                        if (bytesRead > 0)
                        {
                            Array.Copy(tempBuffer, 0, buffer, offset, bytesRead);
                        }
                    }
                }
                finally
                {
                    Marshal.FreeCoTaskMem(ptr);
                }
            }
            return bytesRead;
        }

        /// <summary>Reads a single byte from the stream, moving the current position ahead by 1.</summary>
        /// <returns>A single byte from the stream, -1 if end of stream.</returns>
        public override int ReadByte()
        {
            ThrowIfDisposed();

            var buffer = new byte[1];
            if (Read(buffer, 0, 1) > 0) { return buffer[0]; }
            return -1;
        }

        /// <summary>Seeks within the underlying IStream.</summary>
        /// <param name="offset">Offset</param>
        /// <param name="origin">Where to start seeking</param>
        /// <returns></returns>
        public override long Seek(long offset, SeekOrigin origin)
        {
            ThrowIfDisposed();
            var ptr = Marshal.AllocCoTaskMem(sizeof(long));
            try
            {
                _stream.Seek(offset, (int)origin, ptr);
                return Marshal.ReadInt64(ptr);
            }
            finally
            {
                Marshal.FreeCoTaskMem(ptr);
            }
        }

        /// <summary>Sets the length of the stream</summary>
        /// <param name="value"></param>
        public override void SetLength(long value)
        {
            ThrowIfDisposed();
            _stream.SetSize(value);
        }

        /// <summary>Writes a buffer to the stream if able to do so.</summary>
        /// <param name="buffer">Buffer to write</param>
        /// <param name="offset">Offset in buffer to start writing</param>
        /// <param name="count">Number of bytes to write to the stream</param>
        public override void Write(byte[] buffer, int offset, int count)
        {
            ThrowIfDisposed();

            if (_isReadOnly) { throw new InvalidOperationException(LocalizedMessages.StorageStreamIsReadonly); }
            if (buffer == null) { throw new ArgumentNullException("buffer"); }
            if (offset < 0) { throw new ArgumentOutOfRangeException("offset", LocalizedMessages.StorageStreamOffsetLessThanZero); }
            if (count < 0) { throw new ArgumentOutOfRangeException("count", LocalizedMessages.StorageStreamCountLessThanZero); }
            if (offset + count > buffer.Length) { throw new ArgumentException(LocalizedMessages.StorageStreamBufferOverflow, "count"); }

            if (count > 0)
            {
                var ptr = Marshal.AllocCoTaskMem(sizeof(ulong));
                try
                {
                    if (offset == 0)
                    {
                        _stream.Write(buffer, count, ptr);
                    }
                    else
                    {
                        var tempBuffer = new byte[count];
                        Array.Copy(buffer, offset, tempBuffer, 0, count);
                        _stream.Write(tempBuffer, count, ptr);
                    }
                }
                finally
                {
                    Marshal.FreeCoTaskMem(ptr);
                }
            }
        }

        /// <summary>Writes a single byte to the stream</summary>
        /// <param name="value">Byte to write to stream</param>
        public override void WriteByte(byte value)
        {
            ThrowIfDisposed();
            var buffer = new byte[] { value };
            Write(buffer, 0, 1);
        }

        /// <summary>Disposes the stream.</summary>
        /// <param name="disposing">True if called from Dispose(), false if called from finalizer.</param>
        protected override void Dispose(bool disposing)
        {
            _stream = null;
            base.Dispose(disposing);
        }

        private void ThrowIfDisposed()
        {
            if (_stream == null) throw new ObjectDisposedException(GetType().Name);
        }
    }
}